package com.ship.core.processor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.http.*;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.web.HttpMediaTypeNotSupportedException;

import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.lang.reflect.Type;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;

/**
 * RequestBody读取类
 */
public class RequestBodyRead {
    private static final Object NO_VALUE = new Object();
    private List<HttpMessageConverter<?>> messageConverters;

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private MyRequestResponseBodyAdviceChain advice;

    private List<MediaType> allSupportedMediaTypes;

    private static final Set<HttpMethod> SUPPORTED_METHODS = EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH);

    public RequestBodyRead(List<HttpMessageConverter<?>> messageConverters, MyRequestResponseBodyAdviceChain advice, List<MediaType> allSupportedMediaTypes){
        this.messageConverters = messageConverters;
        this.advice = advice;
        this.allSupportedMediaTypes = allSupportedMediaTypes;
    }


    public <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter param, Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
        MediaType contentType;
        boolean noContentType = false;
        try {
            contentType = inputMessage.getHeaders().getContentType();
        } catch (InvalidMediaTypeException ex) {
            throw new HttpMediaTypeNotSupportedException(ex.getMessage());
        }
        if (contentType == null) {
            noContentType = true;
            contentType = MediaType.APPLICATION_OCTET_STREAM;
        }

        Class<?> contextClass = (param != null ? param.getContainingClass() : null);
        Class<T> targetClass = (targetType instanceof Class<?> ? (Class<T>) targetType : null);
        if (targetClass == null) {
            ResolvableType resolvableType = (param != null ?
                    ResolvableType.forMethodParameter(param) : ResolvableType.forType(targetType));
            targetClass = (Class<T>) resolvableType.resolve();
        }

        HttpMethod httpMethod = ((HttpRequest) inputMessage).getMethod();
        Object body = NO_VALUE;

        try {
            inputMessage = new EmptyBodyCheckingHttpInputMessage(inputMessage);

            for (HttpMessageConverter<?> converter : this.messageConverters) {
                Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
                if (converter instanceof GsonHttpMessageConverter) {
                    GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter;
                    if (genericConverter.canRead(targetType, contextClass, contentType)) {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
                        }
                        if (inputMessage.getBody() != null) {
                            inputMessage = advice.beforeBodyRead(inputMessage, param, targetType, converterType);
                            body = genericConverter.read(targetType, contextClass, inputMessage);
                            body = advice.afterBodyRead(body, inputMessage, param, targetType, converterType);
                        } else {
                            body = null;
                            body = advice.handleEmptyBody(body, inputMessage, param, targetType, converterType);
                        }
                        break;
                    }
                }
            }
        } catch (IOException ex) {
            throw new HttpMessageNotReadableException("Could not read document: " + ex.getMessage(), ex);
        }

        if (body == NO_VALUE) {
            if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
                    (noContentType && inputMessage.getBody() == null)) {
                return null;
            }
            throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
        }

        return body;
    }



    private static class EmptyBodyCheckingHttpInputMessage implements HttpInputMessage {

        private final HttpHeaders headers;

        private final InputStream body;

        private final HttpMethod method;


        public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
            this.headers = inputMessage.getHeaders();
            InputStream inputStream = inputMessage.getBody();
            if (inputStream == null) {
                this.body = null;
            } else if (inputStream.markSupported()) {
                inputStream.mark(1);
                this.body = (inputStream.read() != -1 ? inputStream : null);
                inputStream.reset();
            } else {
                PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
                int b = pushbackInputStream.read();
                if (b == -1) {
                    this.body = null;
                } else {
                    this.body = pushbackInputStream;
                    pushbackInputStream.unread(b);
                }
            }
            this.method = ((HttpRequest) inputMessage).getMethod();
        }

        @Override
        public HttpHeaders getHeaders() {
            return this.headers;
        }

        @Override
        public InputStream getBody() throws IOException {
            return this.body;
        }

        public HttpMethod getMethod() {
            return this.method;
        }
    }

}
